package com.frp.lib.utils;

/**
 * Created by hujianjun on 11/29 0029.
 */

import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.StreamCipher;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.CFBBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

/**
 * 加密工具类
 *
 * <pre>
 * AES支持128/192/256,取决于密钥长度(与位数对应)
 * DES密钥长度8字节
 * 3DES密钥长度24字节
 *
 * 采用CBC 需指定初始向量IV，长度与分组大小相同
 * DES为8字节；AES为16字节
 *
 * </pre>
 */
public class Crypto {
    static {
//        // add bouncycastle support for md4 etc..
//        Security.addProvider(new BouncyCastleProvider());
    }
    public static enum CryptType {
        DES_ECB_PKCS5("DES/ECB/PKCS5Padding"),
        DES_CBC_PKCS5("DES/CBC/PKCS5Padding", 8),
        DESede_ECB_PKCS5("DESede/ECB/PKCS5Padding"),
        DESede_CBC_PKCS5("DESede/CBC/PKCS5Padding", 8),
        AES_ECB_PKCS5("AES/CBC/PKCS5Padding", 16),
        AES_CBC_PKCS5("AES/CBC/PKCS5Padding", 16),
        AES_CBC_PKCS7("AES/CBC/PKCS7Padding", 16),
        AES_CFB_NoPadding("AES/CFB/NoPadding", 16);
        public final String algorithm;
        public final String keyAlg;
        public final int ivlen;
        private CryptType(String algorithm, int ivlen) {
            this.algorithm = algorithm;
            this.keyAlg = this.algorithm.substring(0, this.algorithm.indexOf('/'));
            this.ivlen = ivlen;
        }
        private CryptType(String algorithm) {
            this(algorithm, 0);
        }
        @Override
        public String toString() {
            return this.algorithm;
        }
    }

    /**
     * Initialize the key
     *
     * @param type
     * @return
     */
    public static String initKey(CryptType type) {
        try {
            KeyGenerator generator = KeyGenerator.getInstance(type.keyAlg);
            SecretKey secretKey = generator.generateKey();
            byte[] key = secretKey.getEncoded();
            return Codec.bytesToHexString(key);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static byte[] encryptPBKDF2(char[] msg, byte[] salt, int iter, int keyLen) {
        PBEKeySpec pbeKeySpec = new PBEKeySpec(msg, salt, iter, keyLen);
        try {
            SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            SecretKey secretKey = skf.generateSecret(pbeKeySpec);
            byte[] key = secretKey.getEncoded();
            return key;
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException(e);
        }
    }

    public static Cipher buildCipherCFB(byte[] key, byte[] iv, int MODE) {
        try {
            SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
            Cipher cipher = Cipher.getInstance("AES_128/CFB/NoPadding");
            IvParameterSpec ivparamSpec = null;
            if (iv != null) {
                ivparamSpec = new IvParameterSpec(iv);
            }
            cipher.init(MODE, skeySpec, ivparamSpec);
            return cipher;
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    public static byte[] encryptCFB(byte[] value, byte[] key, byte[] iv) {
        Cipher cipher = buildCipherCFB(key, iv, Cipher.ENCRYPT_MODE);
        try {
            return cipher.doFinal(value);
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
        return null;
//        return cipher.update(value);
    }

    public static byte[] decryptCFB(byte[] value, byte[] key, byte[] iv) {
        Cipher cipher = buildCipherCFB(key, iv, Cipher.DECRYPT_MODE);
        try {
            return cipher.doFinal(value);
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
        return null;
//        return cipher.update(value);
    }

    public static StreamCipher buildCipherCFB_BC(byte[] key, byte[] iv, int bitsBlock, boolean encrypt) {
        BlockCipher aes = new AESEngine();
        StreamCipher cipher = new CFBBlockCipher(aes, bitsBlock);
        CipherParameters keyP = new KeyParameter(key);
        CipherParameters ivP = new ParametersWithIV(keyP, iv);
        cipher.init(encrypt, ivP);
        return cipher;
    }


    public static String md5(String data) {
        MessageDigest md = null;
        try {
            md = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        md.update(data.getBytes());
        StringBuffer buf = new StringBuffer();
        byte[] bits = md.digest();
        for(int i=0;i<bits.length;i++){
            int a = bits[i];
            if(a<0) a+=256;
            if(a<16) buf.append("0");
            buf.append(Integer.toHexString(a));
        }
        return buf.toString();
    }

    public static String sha1(String data) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA1");
        md.update(data.getBytes());
        StringBuffer buf = new StringBuffer();
        byte[] bits = md.digest();
        for(int i=0;i<bits.length;i++){
            int a = bits[i];
            if(a<0) a+=256;
            if(a<16) buf.append("0");
            buf.append(Integer.toHexString(a));
        }
        return buf.toString();
    }

    /**
     * generate default ivparam for type
     *
     * @return
     */
    public static byte[] generateDefaultIv(CryptType type) {
        byte[] iv = new byte[type.ivlen];
        for (int i = 0; i < iv.length; i++) {
            iv[i] = 0x01;
        }
        return iv;
    }
    /**
     * Encrypt the value with the encryption standard.
     *
     * @param value
     *            raw string
     * @param key
     *            in hex format
     * @param iv
     *            in hex format if exist
     * @param type
     * @return result in hex format
     */
    public static String encrypt(String value, String key, String iv, CryptType type) {
        byte[] dvalue;
        try {
            dvalue = value.getBytes("utf-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        byte[] dkey = Codec.hexStringToByte(key);
        byte[] div = null;
        if (iv != null && iv.length() > 0) {
            div = Codec.hexStringToByte(iv);
        }
        byte[] result = encrypt(dvalue, dkey, div, type);
        return Codec.bytesToHexString(result);
    }
    /**
     * Encrypt the value with the encryption standard.
     *
     * <pre>
     * key must have the corresponding length.
     *
     * if use cbc mode which need iv param, the iv must not be null,
     * and iv data length is 16 for aes, 8 for des
     *
     * </pre>
     *
     * @param value
     * @param key
     * @param iv
     * @return
     */
    public static byte[] encrypt(byte[] value, byte[] key, byte[] iv, CryptType type) {
        try {
            SecretKeySpec skeySpec = new SecretKeySpec(key, type.keyAlg);
            Cipher cipher = Cipher.getInstance(type.algorithm);
            IvParameterSpec ivparamSpec = null;
            if (iv != null) {
                ivparamSpec = new IvParameterSpec(iv);
            }
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, ivparamSpec);
            return cipher.doFinal(value);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
    /**
     * Encrypt the value with the encryption standard.
     *
     * @param value
     *            encoded data in hex format
     * @param key
     *            in hex format
     * @param iv
     *            in hex format if exist
     * @param type
     * @return result raw string
     */
    public static String decrypt(String value, String key, String iv, CryptType type) {
        byte[] dvalue = Codec.hexStringToByte(value);
        byte[] dkey = Codec.hexStringToByte(key);
        byte[] div = null;
        if (iv != null && iv.length() > 0) {
            div = Codec.hexStringToByte(iv);
        }
        byte[] result = decrypt(dvalue, dkey, div, type);
        try {
            return new String(result, "utf-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * Decrypt the value with the encryption standard.
     *
     * <pre>
     * key must have the corresponding length.
     *
     * if use cbc mode which need iv param, the iv must not be null,
     * and iv data length is 16 for aes, 8 for des
     *
     * </pre>
     *
     * @param value
     * @param key
     * @param iv
     * @param type
     * @return
     */
    public static byte[] decrypt(byte[] value, byte[] key, byte[] iv, CryptType type) {
        try {
            SecretKeySpec skeySpec = new SecretKeySpec(key, type.keyAlg);
            Cipher cipher = Cipher.getInstance(type.algorithm);
            IvParameterSpec ivparamSpec = null;
            if (iv != null) {
                ivparamSpec = new IvParameterSpec(iv);
            }
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivparamSpec);
            return cipher.doFinal(value);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
}